<?php

namespace CMS;

class Email {
	private $smtp = false;

	public function __construct() {
		if(EMAIL_Smtp === true)
			$this->smtp = new Smtp(SMTP_Server, SMTP_Port, SMTP_Username, SMTP_Password, SMTP_Secure);
	}

	// send the email
	public function send($email, $name, $template) {
		// send via smtp
		if($this->smtp)
			return $this->smtp->send($email, $template[0], $template[1], array('To' => "{$name} <{$email}>", 'From' => EMAIL_From));

		// build headers
		$headers = array (
			'To' => "{$name} <{$email}>",
			'From' => EMAIL_From,
			'Date' => gmdate('r'),
			'MIME-Version' => '1.0',
			'Content-Type' => 'text/plain; charset=UTF-8',
			'Content-transfer-encoding' => 'charset=utf-8',
			'X-Mailer' => 'Sunny Mailer'
		);

		// parse headers
		$content = '';
		foreach($headers as $name=>$value)
			$content .= $name . ': ' . $value . Smtp::EOL;

		// send via php function mail
		return mail($email, $template[0], $template[1], $content);
	}
}

class Smtp {
	const NONE = 0;
	const TLS = 1;
	const SSL = 2;

	const EOL = "\r\n";

	protected $server;
	protected $port;
	protected $username = null;
	protected $password = null;
	protected $secure = 0;

	protected $conn = null;
	protected $last_line = null;

	protected $servername = 'localhost';

	public $timeout = 30;

	protected function _read() {
		$data = '';

		while($str = fgets($this->conn, 4096)) {
			$data .= $str;

			if($str[3] == ' ')
				break;
		}

		return $data;
	}

	protected function _readCode($data = null) {
		if(is_null($data)) {
			$data = $this->_read();
			$this->last_line = $data;
		}

		return substr($data, 0, 3);
	}

	protected function _write($data, $eol = true) {
		fputs($this->conn, $data . ($eol ? self::EOL : ''));
	}

	public function __construct($server = 'localhost', $port = 25, $username = null, $password = null, $secure = self::NONE) {
		$this->server = $secure == self::SSL ? 'ssl://' . $server : $server;
		$this->port = $port;
		$this->username = $username;
		$this->password = $password;
		$this->secure = (int)$secure;
		$this->servername = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : gethostname();
	}

	public function __destruct() {
		$this->disconnect();
	}

	public function disconnect() {
		if(is_null($this->conn))
			return;

		$this->_write('QUIT');
		$this->_read();
		fclose($this->conn);

		$this->conn = null;
		$this->last_line = null;
		return true;
	}

	public function connect() {
		$this->conn = fsockopen($this->server, $this->port, $errno, $errstr, $this->timeout);

		if(!$this->conn)
			throw new Smtp_Exception('Unable to connect to server ' . $this->server . ': ' . $errno . ' - ' . $errstr);

		if($this->_readCode() != 220)
			throw new Smtp_Exception('SMTP error: '.$this->last_line);

		return true;
	}

	public function authenticate() {
		$this->_write('HELO '.$this->servername);
		$this->_read();

		if($this->secure == self::TLS) {
			$this->_write('STARTTLS');

			if($this->_readCode() != 220)
				throw new Smtp_Exception('Can\'t start TLS session: '.$this->last_line);

			stream_socket_enable_crypto($this->conn, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);

			$this->_write('HELO ' . $this->servername);

			if($this->_readCode() != 250)
				throw new Smtp_Exception('SMTP error on HELO: '.$this->last_line);
		}

		if(!is_null($this->username) && !is_null($this->password)) {
			$this->_write('AUTH LOGIN');

			if($this->_readCode() != 334)
				throw new Smtp_Exception('SMTP AUTH error: '.$this->last_line);

			$this->_write(base64_encode($this->username));

			if($this->_readCode() != 334)
				throw new Smtp_Exception('SMTP AUTH error: '.$this->last_line);

			$this->_write(base64_encode($this->password));

			if($this->_readCode() != 235)
				throw new Smtp_Exception('SMTP AUTH error: '.$this->last_line);
		}

		return true;
	}

	public function send($to, $subject, $message, $headers = array()) {
		if(is_null($this->conn)) {
			$this->connect();
			$this->authenticate();
		}

		// Parse $headers if it's a string
		if(is_string($headers)) {
			preg_match_all('/^(\\S.*?):(.*?)\\s*(?=^\\S|\\Z)/sm', $headers, $match);
			$headers = array();

			foreach($match as $header)
				$headers[$header[1]] = $header[2];
		}

		// Normalize headers
		$headers_normalized = array();

		foreach($headers as $key=>$value) {
			$key = preg_replace_callback('/^.|(?<=-)./', function($m) { return ucfirst($m[0]); }, strtolower(trim($key)));
			$headers_normalized[$key] = $value;
		}

		$headers = $headers_normalized;
		unset($headers_normalized);

		// Set default headers if they are missing
		if(!isset($headers['Date']))
			$headers['Date'] = date(DATE_RFC822);

		$headers['Subject'] = (trim($subject) == '') ? '' : '=?UTF-8?B?'.base64_encode($subject).'?=';

		if(!isset($headers['MIME-Version']))
			$headers['MIME-Version'] = '1.0';

		if(!isset($headers['Content-Type']))
			$headers['Content-Type'] = 'text/plain; charset=UTF-8';

		if(!isset($headers['From']))
			$headers['From'] = 'mail@'.$this->servername;

		$content = '';

		foreach($headers as $name=>$value)
			$content .= $name . ': ' . $value . self::EOL;

		$content = trim($content) . self::EOL . self::EOL . $message . self::EOL;
		$content = preg_replace("#(?<!\r)\n#si", self::EOL, $content);
		$content = wordwrap($content, 998, self::EOL, true);

		// Take the first sender address for the smtp
		list($from) = self::extractEmailAddresses($headers['From']);

		// Extract and filter recipients addresses
		$to = self::extractEmailAddresses($to);
		$headers['To'] = implode(', ', $to);

		if(isset($headers['Cc'])) {
			$headers['Cc'] = self::extractEmailAddresses($headers['Cc']);
			$to = array_merge($to, $headers['Cc']);

			$headers['Cc'] = implode(', ', $headers['Cc']);
		}

		if(isset($headers['Bcc'])) {
			$headers['Bcc'] = self::extractEmailAddresses($headers['Bcc']);
			$to = array_merge($to, $headers['Bcc']);

			$headers['Bcc'] = implode(', ', $headers['Bcc']);
		}

		$this->_write('MAIL FROM: <'.$from.'>');
		$this->_read();

		foreach($to as $dest) {
			$this->_write('RCPT TO: <'.$dest.'>');
			$this->_read();
		}

		$this->_write('DATA');
		$this->_read();
		$this->_write($content . '.');

		if($this->_readCode() != 250)
			throw new Smtp_Exception('Can\'t send message. SMTP said: ' . $this->last_line);

		return true;
	}

	public static function extractEmailAddresses($str) {
		if(is_array($str)) {
			$out = array();

			// Filter invalid email addresses
			foreach($str as $email)
				if(filter_var($email, FILTER_VALIDATE_EMAIL))
					$out[] = $email;

			return $out;
		}

		$str = explode(',', $str);
		$out = array();

		foreach($str as $s) {
			$s = trim($s);
			if(preg_match('/(?:([\'"]).*?\1\s*)?<([^>]*)>/', $s, $match) && filter_var(trim($match[2]), FILTER_VALIDATE_EMAIL))
				$out[] = trim($match[2]);

			elseif(filter_var($s, FILTER_VALIDATE_EMAIL))
				$out[] = $s;

			else {} // unrecognized, skip
		}

		return $out;
	}
}

class Smtp_Exception extends \Exception {}